﻿// ---------------------------------------------------------------------------------------------
// <copyright file="BatchGenerator.cs" company="Hiyoko">
//   Copyright (c) 2015 All Rights Reserved
// </copyright>
// <summary>
//   This is where the main logic is impelmented.
// </summary>
// ---------------------------------------------------------------------------------------------

using Hiyoko.DependenciesSorter.CycleDetection;
using Hiyoko.DependenciesSorter.Properties;
using Hiyoko.DependenciesSorter.TopologicalSort;
using Microsoft.Xrm.Sdk.Metadata;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using XrmToolBox;

namespace Hiyoko.DependenciesSorter
{
    public partial class DependenciesSorter : PluginBase, ICodePlexPlugin
    {
        #region Private members

        private EntityMetadata[] _entityMetadatas;

        #endregion Private members

        #region Properties

        /// <summary>
        /// Gets the logo to display in the tools list
        /// </summary>
        public override Image PluginLogo
        {
            get { return Resources.GroupBar_80; }
        }

        public string CodePlexUrlName
        {
            get { return "https://xrmdependenciessorter.codeplex.com/"; }
        }

        #endregion Properties

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the BatchGenerator class.
        /// </summary>
        public DependenciesSorter()
        {
            InitializeComponent();

            var item = lvIgnoredEntities.Items.Add("systemuser");
            item.Checked = true;

            this.ConnectionUpdated += new ConnectionUpdatedHandler(ResetMetadatas);
        }

        #endregion Constructor

        #region Events

        /// <summary>
        /// On connection updated, metadatas are reset.
        /// </summary>
        /// <param name="sender">The object sender</param>
        /// <param name="e">The event arguments</param>
        private void ResetMetadatas(object sender, EventArgs e)
        {
            _entityMetadatas = null;

            tsbSortEntities.Enabled = false;

            lvIgnoredEntities.BeginUpdate();
            lvIgnoredEntities.Items.Clear();
            var item = lvIgnoredEntities.Items.Add("systemuser");
            item.Checked = true;
            lvIgnoredEntities.EndUpdate();

            tvIgnoredDependencies.BeginUpdate();
            tvIgnoredDependencies.Nodes.Clear();
            tvIgnoredDependencies.EndUpdate();

            lStatus.ForeColor = Color.Black;
            lStatus.Text = "Click on Detect Circular Dependencies to begin.";
        }

        /// <summary>
        /// On click, detects circular dependencies.
        /// </summary>
        /// <param name="sender">The object sender</param>
        /// <param name="e">The event arguments</param>
        private void BtnDetectCircularDependencies_Click(object sender, EventArgs e)
        {
            ExecuteMethod(ProcessDetectCircularDependencies);
        }

        /// <summary>
        /// On click, sorts entities.
        /// </summary>
        /// <param name="sender">The object sender</param>
        /// <param name="e">The event arguments</param>
        private void BtnSortEntities_Click(object sender, EventArgs e)
        {
            ExecuteMethod(ProcessSortEntities);
        }

        /// <summary>
        /// On check, checks all child nodes.
        /// </summary>
        /// <param name="sender">The object sender</param>
        /// <param name="e">The event arguments</param>
        private void tvIgnoredDependencies_AfterCheck(object sender, TreeViewEventArgs e)
        {
            var stack = new Stack<TreeNode>();
            stack.Push(e.Node);

            while (stack.Count > 0)
            {
                var node = stack.Pop();
                if (node != e.Node)
                {
                    node.Checked = e.Node.Checked;
                }

                foreach (TreeNode childNode in node.Nodes)
                {
                    stack.Push(childNode);
                }
            }
        }

        /// <summary>
        /// On click, closes the plugin tab.
        /// </summary>
        /// <param name="sender">The object sender</param>
        /// <param name="e">The event arguments</param>
        private void BtnClose_Click(object sender, EventArgs e)
        {
            CloseTool();
        }

        #endregion Events

        #region Private methods

        /// <summary>
        /// Detects circular dependencies.
        /// </summary>
        private void ProcessDetectCircularDependencies()
        {
            WorkAsync("Detecting circular dependencies...",
                (w, e) => // Work To Do Asynchronously
                {
                    w.ReportProgress(0, "Retrieving entities metadata...");
                    if (_entityMetadatas == null)
                    {
                        _entityMetadatas = CrmMetadataHelper.GetEntitiesMetadata(Service);
                    }

                    w.ReportProgress(30, "Ignoring non importable entities...");
                    var importableEntityMetadatas = _entityMetadatas.Where(em => em.IsImportable.HasValue && em.IsImportable.Value).ToArray();

                    w.ReportProgress(40, "Ignoring specified entities...");
                    var ignoredEntities = GetCheckedIgnoredEntities();

                    w.ReportProgress(50, "Ignoring specified dependencies...");
                    var ignoredDependencies = GetCheckedIgnoredDependencies();

                    w.ReportProgress(60, "Detecting circular dependencies...");
                    var cycleDetectionManager = new CycleDetectionManager(importableEntityMetadatas, ignoredEntities, ignoredDependencies);
                    var cycles = cycleDetectionManager.DetectCycles();

                    w.ReportProgress(99, "Finished!");
                    e.Result = cycles;
                },
                e => // Finished Async Call.  Cleanup
                {
                    var cycles = (List<Cycle>)e.Result;

                    RefreshIgnoredItems(cycles);

                    if (!cycles.Any())
                    {
                        lStatus.ForeColor = Color.Green;
                        lStatus.Text = "No circular dependencies found. Click Sort Entities to continue.";
                        tsbSortEntities.Enabled = true;
                    }
                },
                e => // Logic wants to display an update.  This gets called when ReportProgress Gets Called
                {
                    SetWorkingMessage(e.UserState.ToString());
                }
            );
        }

        /// <summary>
        /// Sorts entities.
        /// </summary>
        private void ProcessSortEntities()
        {
            WorkAsync("Sorting entities...",
                (w, e) => // Work To Do Asynchronously
                {
                    w.ReportProgress(0, "Retrieving entities metadata...");
                    if (_entityMetadatas == null)
                    {
                        _entityMetadatas = CrmMetadataHelper.GetEntitiesMetadata(Service);
                    }

                    w.ReportProgress(30, "Ignoring non importable entities...");
                    var importableEntityMetadatas = _entityMetadatas.Where(em => em.IsImportable.HasValue && em.IsImportable.Value).ToArray();

                    w.ReportProgress(40, "Ignoring specified entities...");
                    var ignoredEntities = GetCheckedIgnoredEntities();

                    w.ReportProgress(50, "Ignoring specified dependencies...");
                    var ignoredDependencies = GetCheckedIgnoredDependencies();

                    w.ReportProgress(60, "Initializing topological sorter...");
                    var topologicalSorter = new TopologicalSorterManager(importableEntityMetadatas, ignoredEntities, ignoredDependencies);

                    w.ReportProgress(70, "Sorting entities...");
                    var sortedProcesses = new List<IEnumerable<OrderedProcess>>();
                    sortedProcesses = topologicalSorter.Sort().ToList();

                    w.ReportProgress(99, "Finished!");
                    e.Result = sortedProcesses;
                },
                e => // Finished Async Call.  Cleanup
                {
                    var sortedProcesses = (List<IEnumerable<OrderedProcess>>)e.Result;
                    if (!sortedProcesses.Any())
                    {
                        throw new ApplicationException("An error occured while processing sorted processes.");
                    }
                    SaveProcessedEntitiesToFile(sortedProcesses);

                    lStatus.ForeColor = Color.Green;
                    lStatus.Text = "File saved to disk. Many thanks for using this cool plugin.";
                },
                e => // Logic wants to display an update.  This gets called when ReportProgress Gets Called
                {
                    SetWorkingMessage(e.UserState.ToString());
                }
            );
        }

        /// <summary>
        /// Refreshes ignored items.
        /// </summary>
        /// <param name="cycles">The detected cycles</param>
        private void RefreshIgnoredItems(List<Cycle> cycles)
        {
            if (cycles == null)
            {
                throw new ArgumentException("The argument <cycles> is null.");
            }

            lStatus.ForeColor = Color.Red;
            lStatus.Text = string.Format("{0} cycle(s) detected. Please ignore relevant entities and/or dependencies and retry.", cycles.Count);

            // Refresh list of ignored entities.
            lvIgnoredEntities.BeginUpdate();
            foreach (var cycle in cycles)
            {
                foreach (var component in cycle.Components)
                {
                    var item = lvIgnoredEntities.FindItemWithText(component.Name, false, 0, false);
                    if (item == null)
                    {
                        lvIgnoredEntities.Items.Add(component.Name);
                    }
                }
            }
            lvIgnoredEntities.EndUpdate();

            // Refresh list of checked ignored dependencies.
            var checkedIgnoredDependencies = GetCheckedIgnoredDependencies();
            tvIgnoredDependencies.BeginUpdate();
            tvIgnoredDependencies.Nodes.Clear();

            if (checkedIgnoredDependencies.Count > 0)
            {
                var cycleNode = tvIgnoredDependencies.Nodes.Add("Previous circular dependencies ignored:");
                foreach (var ignoredDependency in checkedIgnoredDependencies)
                {
                    var entityNode = cycleNode.Nodes.Add(ignoredDependency.Key);
                    foreach (var dependency in ignoredDependency.Value)
                    {
                        entityNode.Nodes.Add(dependency);
                    }
                }
                cycleNode.Checked = true;
                tvIgnoredDependencies_AfterCheck(this, new TreeViewEventArgs(cycleNode, TreeViewAction.Unknown));
            }

            // Refresh list of to be defined ignored dependencies.
            var cycleId = 1;
            foreach (var cycle in cycles)
            {
                var cycleNode = tvIgnoredDependencies.Nodes.Add("Circular dependencies " + cycleId);
                foreach (var component in cycle.Components)
                {
                    var entityNode = cycleNode.Nodes.Add(component.Name);
                    foreach (var dependency in component.Dependencies)
                    {
                        var DependencyNode = entityNode.Nodes.Add(dependency);
                    }
                }
                cycleId++;
            }
            tvIgnoredDependencies.EndUpdate();
        }

        /// <summary>
        /// Gets checked ignored entities.
        /// </summary>
        /// <returns></returns>
        private List<string> GetCheckedIgnoredEntities()
        {
            var ignoredEntities = new List<string>();

            foreach (ListViewItem item in lvIgnoredEntities.Items)
            {
                if (item.Checked)
                {
                    ignoredEntities.Add(item.Text);
                }
            }

            return ignoredEntities;
        }

        /// <summary>
        /// Gets checked ignored dependencies.
        /// </summary>
        /// <returns></returns>
        private Dictionary<string, List<string>> GetCheckedIgnoredDependencies()
        {
            var ignoredDependencies = new Dictionary<string, List<string>>();

            foreach (TreeNode cycle in tvIgnoredDependencies.Nodes)
            {
                foreach (TreeNode entity in cycle.Nodes)
                {
                    var logicalName = entity.Text;
                    foreach (TreeNode dependency in entity.Nodes)
                    {
                        var dependencyLogicalName = dependency.Text;
                        if (dependency.Checked)
                        {
                            if (!ignoredDependencies.ContainsKey(logicalName))
                            {
                                ignoredDependencies.Add(logicalName, new List<string>());
                            }

                            if (!ignoredDependencies[logicalName].Contains(dependencyLogicalName))
                            {
                                ignoredDependencies[logicalName].Add(dependencyLogicalName);
                            }
                        }
                    }
                }
            }

            return ignoredDependencies;
        }

        /// <summary>
        /// Saves processed entities to file.
        /// </summary>
        /// <param name="sortedProcesses">The processed entities</param>
        private void SaveProcessedEntitiesToFile(List<IEnumerable<OrderedProcess>> sortedProcesses)
        {
            if (sortedProcesses == null)
            {
                throw new ArgumentException("The argument <sortedProcesses> is null.");
            }

            var saveFileDialog = new SaveFileDialog
            {
                Filter = @"CSV files (*.csv)|*.csv|All files (*.*)|*.*",
                FilterIndex = 1,
                RestoreDirectory = true
            };

            if (saveFileDialog.ShowDialog() != DialogResult.OK)
            {
                return;
            }

            var sb = new StringBuilder();
            sb.AppendLine("Batch,LogicalName");
            sb.AppendLine("0,systemuser");

            for (var i = 0; i < sortedProcesses.Count(); i++)
            {
                var processes = sortedProcesses.Skip(i).First();
                foreach (var orderedProcess in processes)
                {
                    sb.AppendLine(string.Format("{0},{1}", i + 1, orderedProcess.Name));
                }
            }

            using (var stream = saveFileDialog.OpenFile())
            using (var writer = new StreamWriter(stream))
            {
                writer.Write(sb.ToString());
            }
        }

        #endregion Private methods
    }
}
